/*
 * $AIST_Release: 0.9.0 $
 * Copyright 2011 Information Technology Research Institute, National
 * Institute of Advanced Industrial Science and Technology
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *    http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package util

import scala.collection.immutable.Map
import java.lang.Exception

/**
 * Command line option parser.
 */
object OptionParser {
  sealed trait OptionType
  object NoArg  extends OptionType
  object OptArg extends OptionType
  object ReqArg extends OptionType

  class OptException(message: String)      extends Exception(message)
  class UnkownOptionException(opt: String) extends OptException("Invalid option: \"%s\"".format(opt))
  class UnexpectArgException(opt: String)  extends OptException("Option \"%s\" doesn't allow an argument".format(opt))
  class NoArgException(opt: String)        extends OptException("Option \"%s\" requires an argument".format(opt))

  /**
   * Command line option descriptor.
   */ 
  case class OptDesc[+T](
      id: T,
      shorts: List[Char],
      longs: List[String], 
      optType: OptionType, 
      explanation: String
    )

  /**
   * A result of parsing command-line option.
   */
  class ParseResult[T](opts: Map[T, Option[String]], val args: List[String]) {
    /**
     * Is there the specified option in command line.
     */
    def hasOpt(opt: T): Boolean        = opts.contains(opt)

    /**
     * Gets the argument of the specified option.
     * When the argument does not exist, returns None.
     */
    def getArg(opt: T): Option[String] = opts.get(opt).flatMap(identity)

    /**
     * Gets the argument of the specified option.
     * When the argument does not exist, returns 'devVal'.
     */
    def getArgOrElse(opt: T, defVal: String): String = getArg(opt).getOrElse(defVal)
  }

  /**
   * Parses command line options.
   * @param desc The descriptors of option. 
   * @param argv The target of parsing. 
   *
   * @throws UnkownOptionException
   * @throws UnexpectArgException
   * @throws NoArgException
   */
  def parseOpt[T](descs: List[OptDesc[T]], argv: List[String]) = {
    def handleOpt(rslt: Map[T, Option[String]], od: OptDesc[T], oname: String, oarg: Option[String]) = {
      od.optType match {
        case NoArg  => if (oarg.isEmpty) rslt.updated(od.id, None)
                       else              throw new UnexpectArgException(oname)
        case OptArg =>                   rslt.updated(od.id, oarg)
        case ReqArg => if (oarg.isEmpty) throw new NoArgException(oname)
                       else              rslt.updated(od.id, oarg)
      }
    }
    def parseLong(opt: String): (String, Option[String]) = {
      assert(opt.startsWith("--"))
      opt.drop(2).split("=", 2) match {
        case Array(o, a) => (o, Some(a))
        case Array(o)    => (o, None)
      }
    }
    def handleShort(rslt: Map[T, Option[String]], head: Char, tail: String, rest: List[String]): (Map[T, Option[String]], List[String]) = {
      (descs.find {_.shorts.contains(head) }) match {
        case Some(od) => 
          if (od.optType == NoArg) {
            if (tail.nonEmpty) handleShort(rslt.updated(od.id, None), tail(0), tail.drop(1), rest)
            else               (rslt.updated(od.id, None), rest)
          } else {
            val (arg, rest1) = if (tail.isEmpty) (rest.headOption, rest.drop(1)) 
                               else              (Some(tail), rest)
            (handleOpt(rslt, od, "-" + head, arg), rest1)
          }
        case None => throw new UnkownOptionException(head.toString)
      }
    }
    def parse1(rslt: Map[T, Option[String]], argv: List[String]): ParseResult[T] = {
      argv match {
        case "--"::xs => new ParseResult(rslt, xs)
        case "-"::_   => new ParseResult(rslt, argv)
        case x::xs => 
          if (x.startsWith("--")) {
            val (oname, oarg) = parseLong(x)
            (descs.find { _.longs.contains(oname) }) match {
              case Some(od) => parse1(handleOpt(rslt, od, oname, oarg), xs)
              case None => throw new UnkownOptionException(oname)
            }
          } else if (x.startsWith("-")) {
            val (r, rest) = handleShort(rslt, x(1), x.drop(2), xs)
            parse1(r, rest)
          } else {
            new ParseResult(rslt, argv)
          }
        case Nil => new ParseResult(rslt, Nil)
      }
    }
    parse1(Map(), argv)
  }

  def usage[T](descs: List[OptDesc[T]]): String = {
    val strs = descs.map { x =>
      ((x.shorts.map{"-" + _} ++ x.longs.map{"--" + _}).mkString(", "), x.explanation)
    }
    val m = strs.map{_._1.length}.max

    strs.map { case (x, expl) =>
      "\t%s %s".format(x + " " * (m - x.length), expl)
    }.mkString("\n")
  }
}
